package com.mazaiting.gateway.security;

import cn.hutool.core.util.StrUtil;
import com.mazaiting.auth.constant.SecurityConst;
import com.mazaiting.common.core.domain.result.ResultCode;
import com.mazaiting.gateway.domain.SecureIgnoreBean;
import com.mazaiting.gateway.util.WebFluxUtil;
import com.mazaiting.redis.service.IRedisStringService;
import com.nimbusds.jose.JWSObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.PathContainer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.pattern.PathPatternParser;
import reactor.core.publisher.Mono;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;

/**
 * 安全拦截全局过滤器，非网关鉴权的逻辑
 * 在ResourceServerManager#check鉴权善后黑名单拦截
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class GatewayGlobalFilter implements GlobalFilter, Ordered {

    private final IRedisStringService redisStringService;

    private final SecureIgnoreBean secureIgnoreBean;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        // 获取响应
        ServerHttpResponse response = exchange.getResponse();
        // 请求路径
        String path = request.getURI().getPath();
        log.info("进入GatewayGlobalFilter全局过滤器开始, url: {}", path);

        // 非JWT放行不做后续解析处理
        String token = request.getHeaders().getFirst(SecurityConst.KEY_AUTHORIZATION);
        if (StrUtil.isBlank(token) || !StrUtil.startWithIgnoreCase(token, SecurityConst.JWT_PREFIX)) {
            log.warn("无 authorization 信息, 放行 url: {}", path);
            return afterFilter(exchange, chain, path);
        }

        // 判断是否在忽略 token 的列表中
        boolean ignoreMatch = secureIgnoreBean.getUrls().stream().anyMatch(s -> PathPatternParser.defaultInstance.parse(s).matches(PathContainer.parsePath(path)));
        if (ignoreMatch) {
            log.warn("白名单接口: {}", path);
            return afterFilter(exchange, chain, path);
        }
        return ReactiveSecurityContextHolder.getContext()
                .map(SecurityContext::getAuthentication)
                .cast(JwtAuthenticationToken.class)
                .flatMap(authentication -> {
                    try {
                        // 获取 token
                        Jwt jwt = authentication.getToken();
                        if (Objects.isNull(jwt)) {
                            return WebFluxUtil.writeErrorInfo(response, ResultCode.ACCESS_UNAUTHORIZED);
                        }
                        // 此处 instant 与 date 差 8 个小时, 但由于只比较是否过期, 所以不需要转换为 date
                        Instant expiresAt = jwt.getExpiresAt();
                        if (Objects.isNull(expiresAt) || expiresAt.isBefore(Instant.now())) {
                            return WebFluxUtil.writeErrorInfo(response, ResultCode.TOKEN_INVALID_OR_EXPIRED);
                        }
                        // 获取数据
                        Map<String, Object> claims = ((Jwt) authentication.getPrincipal()).getClaims();
                        // 获取 jti 唯一标识
                        String jti = String.valueOf(claims.get(SecurityConst.KEY_JWT_JTI));
                        // 判断是否在黑名单中
                        Boolean isBlack = redisStringService.hasKey(SecurityConst.TOKEN_BLACKLIST_PREFIX + jti);
                        if (isBlack) {
                            return WebFluxUtil.writeErrorInfo(response, ResultCode.TOKEN_ACCESS_FORBIDDEN);
                        }
                        // 传递 payload 给其他微服务
                        String payload = String.valueOf(JWSObject.parse(jwt.getTokenValue()).getPayload());
                        // 将登录 ID 放在请求头中
                        ServerHttpRequest newRequest = exchange.getRequest()
                                .mutate()
                                .header(SecurityConst.KEY_JWT_PAYLOAD, URLEncoder.encode(payload, StandardCharsets.UTF_8))
                                .build();
                        return chain.filter(exchange.mutate().request(newRequest).build());
                    } catch (Exception e) {
                        log.error("异常url: {}, 信息: {}", path, e.getMessage());
                        return WebFluxUtil.writeErrorInfo(response, ResultCode.TOKEN_ACCESS_FORBIDDEN);
                    }
                }).then(Mono.fromRunnable(() -> {
                    log.info("进入GatewayGlobalFilter全局过滤器结束, url: {}", path);
                }));
    }

    /**
     * 处理过滤器完成后的操作
     *
     * @param exchange 交换机
     * @param chain    过滤器链
     * @param path     请求路径
     * @return void
     */
    private Mono<Void> afterFilter(ServerWebExchange exchange, GatewayFilterChain chain, String path) {
        return chain.filter(exchange).then().then(Mono.fromRunnable(() -> {
            log.info("进入GatewayGlobalFilter全局过滤器结束, url: {}", path);
        }));
    }

    @Override
    public int getOrder() {
        return -1;
    }
}
